/* range.js is part of Aloha Editor project http://aloha-editor.org
 *
 * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor. 
 * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria.
 * Contributors http://aloha-editor.org/contribution.php 
 * 
 * Aloha Editor is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * Aloha Editor is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * 
 * As an additional permission to the GNU GPL version 2, you may distribute
 * non-source (e.g., minimized or compacted) forms of the Aloha-Editor
 * source code without the copy of the GNU GPL normally required,
 * provided you include this license notice and a URL through which
 * recipients can access the Corresponding Source.
 */
// Ensure GENTICS Namespace
window.GENTICS = window.GENTICS || {};
window.GENTICS.Utils = window.GENTICS.Utils || {};

define([
	'jquery',
	'util/dom',
	'util/class',
	'aloha/console',
	'aloha/rangy-core'
], function (jQuery, Dom, Class, console, rangy) {
	"use strict";

	var GENTICS = window.GENTICS;
	var Aloha = window.Aloha;

	function selfAndParentsUntil(container, limit) {
		var parents = [],
			cur;
		if (1 === container.nodeType) {
			cur = container;
		} else {
			cur = container.parentNode;
		}
		for (;;) {
			if (!cur || cur === limit || 9 === cur.nodeType) {
				break;
			}
			if (1 === cur.nodeType) {
				parents.push(cur);
			}
			cur = cur.parentNode;
		}
		return parents;
	}

	/**
	 * @namespace GENTICS.Utils
	 * @class RangeObject
	 * Represents a selection range in the browser that
	 * has some advanced features like selecting the range.
	 * @param {object} param if boolean true is passed, the range will be deducted from the current browser selection.
	 * If another rangeObject is passed, it will be cloned.
	 * If nothing is passed, the rangeObject will be empty.
	 * @constructor
	 */
	GENTICS.Utils.RangeObject = Class.extend({
		_constructor: function (param) {
			// Take the values from the passed object
			if (typeof param === 'object') {
				if (typeof param.startContainer !== 'undefined') {
					this.startContainer = param.startContainer;
				}
				if (typeof param.startOffset !== 'undefined') {
					this.startOffset = param.startOffset;
				}
				if (typeof param.endContainer !== 'undefined') {
					this.endContainer = param.endContainer;
				}
				if (typeof param.endOffset !== 'undefined') {
					this.endOffset = param.endOffset;
				}
			} else if (param === true) {
				this.initializeFromUserSelection();
			}
		},

		/**
		 * DOM object of the start container of the selection.
		 * This is always has to be a DOM text node.
		 * @property startContainer
		 * @type {DOMObject}
		 */
		startContainer: undefined,

		/**
		 * Offset of the selection in the start container
		 * @property startOffset
		 * @type {Integer}
		 */
		startOffset: undefined,

		/**
		 * DOM object of the end container of the selection.
		 * This is always has to be a DOM text node.
		 * @property endContainer
		 * @type {DOMObject}
		 */
		endContainer: undefined,

		/**
		 * Offset of the selection in the end container
		 * @property endOffset
		 * @type {Integer}
		 */
		endOffset: undefined,

		/**
		 * Delete all contents selected by the current range
		 * @param rangeTree a GENTICS.Utils.RangeTree object may be provided to start from. This parameter is optional
		 */
		deleteContents: function () {

			Dom.removeRange(this);

		},

		/**
		 * Output some log
		 * TODO: move this to Aloha.Log
		 * @param message log message to output
		 * @return void
		 * @deprecated
		 * @hide
		 */
		log: function (message) {
			console.deprecated('Utils.RangeObject', 'log() is deprecated. use ' + 'console.log() from module "aloha/console" instead: ' + message);
		},

		/**
		 * Method to test if a range object is collapsed.
		 * A range is considered collapsed if either no endContainer exists or the endContainer/Offset equal startContainer/Offset
		 * @return {boolean} true if collapsed, false otherwise
		 * @method
		 */
		isCollapsed: function () {
			return (!this.endContainer || (this.startContainer === this.endContainer && this.startOffset === this.endOffset));
		},

		/**
		 * Method to (re-)calculate the common ancestor container and to get it.
		 * The common ancestor container is the DOM Object which encloses the
		 * whole range and is nearest to the start and end container objects.
		 * @return {DOMObject} get the common ancestor container
		 * @method
		 */
		getCommonAncestorContainer: function () {
			if (this.commonAncestorContainer) {
				// sometimes it's cached (or was set)
				return this.commonAncestorContainer;
			}
			// if it's not cached, calculate and then cache it
			this.updateCommonAncestorContainer();

			// now return it anyway
			return this.commonAncestorContainer;
		},

		/**
		 * Get the parent elements of the startContainer/endContainer up to the given limit. When the startContainer/endContainer
		 * is no text element, but a node, the node itself is returned as first element.
		 * @param {jQuery} limit limit object (default: body)
		 * @param {boolean} fromStart true to fetch the parents from the startContainer, false for the endContainer
		 * @return {jQuery} parent elements of the startContainer/endContainer as jQuery objects
		 * @method
		 */
		getContainerParents: function (limit, fromEnd) {
			// TODO cache the calculated parents
			var container = fromEnd ? this.endContainer : this.startContainer;
			if (!container) {
				return false;
			}
			return jQuery(selfAndParentsUntil(container, limit ? limit[0] : null));
		},

		/**
		 * Get the parent elements of the startContainer up to the given limit. When the startContainer
		 * is no text element, but a node, the node itself is returned as first element.
		 * @param {jQuery} limit limit object (default: body)
		 * @return {jQuery} parent elements of the startContainer as jQuery objects
		 * @method
		 */
		getStartContainerParents: function (limit) {
			return this.getContainerParents(limit, false);
		},

		/**
		 * Get the parent elements of the endContainer up to the given limit. When the endContainer is
		 * no text element, but a node, the node itself is returned as first element.
		 * @param {jQuery} limit limit object (default: body)
		 * @return {jQuery} parent elements of the endContainer as jQuery objects
		 * @method
		 */
		getEndContainerParents: function (limit) {
			return this.getContainerParents(limit, true);
		},

		/**
		 * TODO: the commonAncestorContainer is not calculated correctly, if either the start or
		 * the endContainer would be the cac itself (e.g. when the startContainer is a textNode
		 * and the endContainer is the startContainer's parent <p>). in this case the cac will be set
		 * to the parent div
		 * Method to update a range object internally
		 * @param commonAncestorContainer (DOM Object); optional Parameter; if set, the parameter
		 * will be used instead of the automatically calculated CAC
		 * @return void
		 * @hide
		 */
		updateCommonAncestorContainer: function (commonAncestorContainer) {
			// if no parameter was passed, calculate it
			if (!commonAncestorContainer) {
				// this will be needed either right now for finding the CAC or later for the crossing index
				var parentsStartContainer = this.getStartContainerParents(),
					parentsEndContainer = this.getEndContainerParents(),
					i;

				// find the crossing between startContainer and endContainer parents (=commonAncestorContainer)
				if (!(parentsStartContainer.length > 0 && parentsEndContainer.length > 0)) {
					console.warn('aloha/range', 'could not find commonAncestorContainer');
					return false;
				}

				for (i = 0; i < parentsStartContainer.length; i++) {
					if (parentsEndContainer.index(parentsStartContainer[i]) != -1) {
						this.commonAncestorContainer = parentsStartContainer[i];
						break;
					}
				}
			} else {
				this.commonAncestorContainer = commonAncestorContainer;
			}

			// if everything went well, return true :-)
			console.debug(commonAncestorContainer ? 'commonAncestorContainer was set successfully' : 'commonAncestorContainer was calculated successfully');
			return true;
		},

		/**
		 * Helper function for selection in IE. Creates a collapsed text range at the given position
		 * @param container container
		 * @param offset offset
		 * @return collapsed text range at that position
		 * @hide
		 */
		getCollapsedIERange: function (container, offset) {
			// create a text range
			var ieRange = document.body.createTextRange(),
				tmpRange,
			    right,
			    parent,
			    left;

			// search to the left for the next element
			left = this.searchElementToLeft(container, offset);
			if (left.element) {
				// found an element, set the start to the end of that element
				tmpRange = document.body.createTextRange();
				tmpRange.moveToElementText(left.element);
				ieRange.setEndPoint('StartToEnd', tmpRange);

				// and correct the start
				if (left.characters !== 0) {
					ieRange.moveStart('character', left.characters);
				} else {
					// this is a hack, when we are at the start of a text node, move the range anyway
					ieRange.moveStart('character', 1);
					ieRange.moveStart('character', -1);
				}
			} else {
				// found nothing to the left, so search right
				right = this.searchElementToRight(container, offset);
				// also found no element to the right, use the container itself
				parent = container.nodeType == 3 ? container.parentNode : container;
				tmpRange = document.body.createTextRange();
				tmpRange.moveToElementText(parent);
				ieRange.setEndPoint('StartToStart', tmpRange);

				// and correct the start
				if (left.characters !== 0) {
					ieRange.moveStart('character', left.characters);
				}
			}
			ieRange.collapse();

			return ieRange;
		},

		/**
		 * Sets the visible selection in the Browser based on the range object.
		 * If the selection is collapsed, this will result in a blinking cursor,
		 * otherwise in a text selection.
		 * @method
		 */
		select: function () {
			var ieRange, endRange, startRange, range, sel;

			if (typeof this.startContainer === 'undefined' || typeof this.endContainer === 'undefined') {
				console.warn('can not select an empty range');
				return false;
			}

			// create a range
			range = rangy.createRange();
			// set start and endContainer
			range.setStart(this.startContainer, this.startOffset);
			range.setEnd(this.endContainer, this.endOffset);

			// update the selection
			sel = rangy.getSelection();
			sel.setSingleRange(range);
		},

		/**
		 * Starting at the given position, search for the next element to the left and count the number of characters are in between
		 * @param container container of the startpoint
		 * @param offset offset of the startpoint in the container
		 * @return object with 'element' (null if no element found) and 'characters'
		 * @hide
		 */
		searchElementToLeft: function (container, offset) {
			var checkElement,
			    characters = 0;

			if (container.nodeType === 3) {
				// start is in a text node
				characters = offset;
				// begin check at the element to the left (if any)
				checkElement = container.previousSibling;
			} else {
				// start is between nodes, begin check at the element to the left (if any)
				if (offset > 0) {
					checkElement = container.childNodes[offset - 1];
				}
			}

			// move to the right until we find an element
			while (checkElement && checkElement.nodeType === 3) {
				characters += checkElement.data.length;
				checkElement = checkElement.previousSibling;
			}

			return {
				'element': checkElement,
				'characters': characters
			};
		},

		/**
		 * Starting at the given position, search for the next element to the right and count the number of characters that are in between
		 * @param container container of the startpoint
		 * @param offset offset of the startpoint in the container
		 * @return object with 'element' (null if no element found) and 'characters'
		 * @hide
		 */
		searchElementToRight: function (container, offset) {
			var checkElement,
			    characters = 0;

			if (container.nodeType === 3) {
				// start is in a text node
				characters = container.data.length - offset;

				// begin check at the element to the right (if any)
				checkElement = container.nextSibling;
			} else {
				// start is between nodes, begin check at the element to the right (if any)
				if (offset < container.childNodes.length) {
					checkElement = container.childNodes[offset];
				}
			}

			// move to the right until we find an element
			while (checkElement && checkElement.nodeType === 3) {
				characters += checkElement.data.length;
				checkElement = checkElement.nextSibling;
			}

			return {
				'element': checkElement,
				'characters': characters
			};
		},

		/**
		 * Method which updates the rangeObject including all extending properties like commonAncestorContainer etc...
		 * TODO: is this method needed here? or should it contain the same code as Aloha.Selection.prototype.SelectionRange.prototype.update?
		 * @return void
		 * @hide
		 */
		update: function (event) {
			console.debug('now updating rangeObject');

			this.initializeFromUserSelection(event);
			this.updateCommonAncestorContainer();
		},

		/**
		 * Initialize the current range object from the user selection of the browser.
		 * @param event which calls the method
		 * @return void
		 * @hide
		 */
		initializeFromUserSelection: function (event) {
			var selection = rangy.getSelection(),
				browserRange;

			if (!selection) {
				return false;
			}

			// check if a ragne exists
			if (!selection.rangeCount) {
				return false;
			}

			// getBrowserRange
			browserRange = selection.getRangeAt(0);
			if (!browserRange) {
				return false;
			}

			// initially set the range to what the browser tells us
			this.startContainer = browserRange.startContainer;
			this.endContainer = browserRange.endContainer;
			this.startOffset = browserRange.startOffset;
			this.endOffset = browserRange.endOffset;

			// now try to correct the range
			this.correctRange();
			return;
		},

		/**
		 * Correct the current range. The general goal of the algorithm is to have start
		 * and end of the range in text nodes if possible and the end of the range never
		 * at the beginning of an element or text node. Details of the algorithm can be
		 * found in the code comments
		 * @method
		 */
		correctRange: function () {
			var adjacentTextNode,
			    textNode,
			    checkedElement,
			    parentNode,
			    offset;

			this.clearCaches();
			if (this.isCollapsed()) {
				// collapsed ranges are treated specially

				// first check if the range is not in a text node
				if (this.startContainer.nodeType === 1) {
					if (this.startOffset > 0 && this.startContainer.childNodes[this.startOffset - 1].nodeType === 3) {
						// when the range is between nodes (container is an element
						// node) and there is a text node to the left -> move into this text
						// node (at the end)
						this.startContainer = this.startContainer.childNodes[this.startOffset - 1];
						this.startOffset = this.startContainer.data.length;
						this.endContainer = this.startContainer;
						this.endOffset = this.startOffset;
						return;
					}

					if (this.startOffset > 0 && this.startContainer.childNodes[this.startOffset - 1].nodeType === 1) {
						// search for the next text node to the left
						adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(this.startContainer, this.startOffset, true);
						if (adjacentTextNode) {
							this.startContainer = this.endContainer = adjacentTextNode;
							this.startOffset = this.endOffset = adjacentTextNode.data.length;
							return;
						}
						// search for the next text node to the right
						adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(this.startContainer, this.startOffset, false);
						if (adjacentTextNode) {
							this.startContainer = this.endContainer = adjacentTextNode;
							this.startOffset = this.endOffset = 0;
							return;
						}
					}

					if (this.startOffset < this.startContainer.childNodes.length && this.startContainer.childNodes[this.startOffset].nodeType === 3) {
						// when the range is between nodes and there is a text node
						// to the right -> move into this text node (at the start)
						this.startContainer = this.startContainer.childNodes[this.startOffset];
						this.startOffset = 0;
						this.endContainer = this.startContainer;
						this.endOffset = 0;
						return;
					}
				}

				// when the selection is in a text node at the start, look for an adjacent text node and if one found, move into that at the end
				if (this.startContainer.nodeType === 3 && this.startOffset === 0) {
					adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(this.startContainer.parentNode, GENTICS.Utils.Dom.getIndexInParent(this.startContainer), true);
					//only move the selection if the adjacentTextNode is inside the current editable
					//the cursor should not be outside the editable
					if (adjacentTextNode && jQuery(adjacentTextNode).closest(Aloha.activeEditable.obj).length > 0) {
						this.startContainer = this.endContainer = adjacentTextNode;
						this.startOffset = this.endOffset = adjacentTextNode.data.length;
					}
				}
			} else {
				// expanded range found

				// correct the start, but only if between nodes
				if (this.startContainer.nodeType === 1) {
					// if there is a text node to the right, move into this
					if (this.startOffset < this.startContainer.childNodes.length && this.startContainer.childNodes[this.startOffset].nodeType === 3) {
						this.startContainer = this.startContainer.childNodes[this.startOffset];
						this.startOffset = 0;
					} else if (this.startOffset < this.startContainer.childNodes.length && this.startContainer.childNodes[this.startOffset].nodeType === 1) {
						// there is an element node to the right, so recursively check all first child nodes until we find a text node
						textNode = false;
						checkedElement = this.startContainer.childNodes[this.startOffset];
						while (textNode === false && checkedElement.childNodes && checkedElement.childNodes.length > 0) {
							// go to the first child of the checked element
							checkedElement = checkedElement.childNodes[0];
							// when this element is a text node, we are done
							if (checkedElement.nodeType === 3) {
								textNode = checkedElement;
							}
						}

						// found a text node, so move into it
						if (textNode !== false) {
							this.startContainer = textNode;
							this.startOffset = 0;
						}
					}
				}

				// check whether the start is inside a text node at the end
				if (this.startContainer.nodeType === 3 && this.startOffset === this.startContainer.data.length) {
					// check whether there is an adjacent text node to the right and if
					// yes, move into it
					adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(this.startContainer.parentNode, GENTICS.Utils.Dom.getIndexInParent(this.startContainer) + 1, false);
					if (adjacentTextNode) {
						this.startContainer = adjacentTextNode;
						this.startOffset = 0;
					}
				}

				// now correct the end
				if (this.endContainer.nodeType === 3 && this.endOffset === 0) {
					// we are in a text node at the start
					if (this.endContainer.previousSibling && this.endContainer.previousSibling.nodeType === 3) {
						// found a text node to the left -> move into it (at the end)
						this.endContainer = this.endContainer.previousSibling;
						this.endOffset = this.endContainer.data.length;
					} else if (this.endContainer.previousSibling && this.endContainer.previousSibling.nodeType === 1 && this.endContainer.parentNode) {
						// found an element node to the left -> move in between
						parentNode = this.endContainer.parentNode;
						for (offset = 0; offset < parentNode.childNodes.length; ++offset) {
							if (parentNode.childNodes[offset] == this.endContainer) {
								this.endOffset = offset;
								break;
							}
						}
						this.endContainer = parentNode;
					}
				}

				if (this.endContainer.nodeType == 1 && this.endOffset === 0) {
					// we are in an element node at the start, possibly move to the previous sibling at the end
					if (this.endContainer.previousSibling) {
						if (this.endContainer.previousSibling.nodeType === 3) {
							// previous sibling is a text node, move end into here (at the end)
							this.endContainer = this.endContainer.previousSibling;
							this.endOffset = this.endContainer.data.length;
						} else if (this.endContainer.previousSibling.nodeType === 1 && this.endContainer.previousSibling.childNodes && this.endContainer.previousSibling.childNodes.length > 0) {
							// previous sibling is another element node with children,
							// move end into here (at the end)
							this.endContainer = this.endContainer.previousSibling;
							this.endOffset = this.endContainer.childNodes.length;
						}
					}
				}

				// correct the end, but only if between nodes
				if (this.endContainer.nodeType == 1) {
					// if there is a text node to the left, move into this
					if (this.endOffset > 0 && this.endContainer.childNodes[this.endOffset - 1].nodeType === 3) {
						this.endContainer = this.endContainer.childNodes[this.endOffset - 1];
						this.endOffset = this.endContainer.data.length;
					} else if (this.endOffset > 0 && this.endContainer.childNodes[this.endOffset - 1].nodeType === 1) {
						// there is an element node to the left, so recursively check all last child nodes until we find a text node
						textNode = false;
						checkedElement = this.endContainer.childNodes[this.endOffset - 1];
						while (textNode === false && checkedElement.childNodes && checkedElement.childNodes.length > 0) {
							// go to the last child of the checked element
							checkedElement = checkedElement.childNodes[checkedElement.childNodes.length - 1];
							// when this element is a text node, we are done
							if (checkedElement.nodeType === 3) {
								textNode = checkedElement;
							}
						}

						// found a text node, so move into it
						if (textNode !== false) {
							this.endContainer = textNode;
							this.endOffset = this.endContainer.data.length;
						}
					}
				}
			}
		},

		/**
		 * Clear the caches for this range. This method must be called when the range itself (start-/endContainer or start-/endOffset) is modified.
		 * @method
		 */
		clearCaches: function () {
			this.commonAncestorContainer = undefined;
		},

		/**
		 * Get the range tree of this range.
		 * The range tree will be cached for every root object. When the range itself is modified, the cache should be cleared by calling GENTICS.Utils.RangeObject.clearCaches
		 * @param {DOMObject} root root object of the range tree, if non given, the common ancestor container of the start and end containers will be used
		 * @return {RangeTree} array of RangeTree object for the given root object
		 * @method
		 */
		getRangeTree: function (root) {
			// TODO cache rangeTrees
			if (typeof root === 'undefined') {
				root = this.getCommonAncestorContainer();
			}

			this.inselection = false;
			return this.recursiveGetRangeTree(root);
		},

		/**
		 * Recursive inner function for generating the range tree.
		 * @param currentObject current DOM object for which the range tree shall be generated
		 * @return array of Tree objects for the children of the current DOM object
		 * @hide
		 */
		recursiveGetRangeTree: function (currentObject) {
			// get all direct children of the given object
			var jQueryCurrentObject = jQuery(currentObject),
				childCount = 0,
				that = this,
				currentElements = [];

			jQueryCurrentObject.contents().each(function (index) {
				var type = 'none',
					startOffset = false,
					endOffset = false,
					collapsedFound = false,
					noneFound = false,
					partialFound = false,
					fullFound = false,
					i;

				// check for collapsed selections between nodes
				if (that.isCollapsed() && currentObject === that.startContainer && that.startOffset === index) {
					// insert an extra rangetree object for the collapsed range here
					currentElements[childCount] = new GENTICS.Utils.RangeTree();
					currentElements[childCount].type = 'collapsed';
					currentElements[childCount].domobj = undefined;
					that.inselection = false;
					collapsedFound = true;
					childCount++;
				}

				if (!that.inselection && !collapsedFound) {
					// the start of the selection was not yet found, so look for it now
					// check whether the start of the selection is found here

					// check is dependent on the node type
					switch (this.nodeType) {
					case 3:
						// text node
						if (this === that.startContainer) {
							// the selection starts here
							that.inselection = true;

							// when the startoffset is > 0, the selection type is only partial
							type = that.startOffset > 0 ? 'partial' : 'full';
							startOffset = that.startOffset;
							endOffset = this.length;
						}
						break;
					case 1:
						// element node
						if (this === that.startContainer && that.startOffset === 0) {
							// the selection starts here
							that.inselection = true;
							type = 'full';
						}
						if (currentObject === that.startContainer && that.startOffset === index) {
							// the selection starts here
							that.inselection = true;
							type = 'full';
						}
						break;
					}
				}

				if (that.inselection && !collapsedFound) {
					if (type == 'none') {
						type = 'full';
					}
					// we already found the start of the selection, so look for the end of the selection now
					// check whether the end of the selection is found here

					switch (this.nodeType) {
					case 3:
						// text node
						if (this === that.endContainer) {
							// the selection ends here
							that.inselection = false;

							// check for partial selection here
							if (that.endOffset < this.length) {
								type = 'partial';
							}
							if (startOffset === false) {
								startOffset = 0;
							}
							endOffset = that.endOffset;
						}
						break;
					case 1:
						// element node
						if (this === that.endContainer && that.endOffset === 0) {
							that.inselection = false;
						}
						break;
					}
					if (currentObject === that.endContainer && that.endOffset <= index) {
						that.inselection = false;
						type = 'none';
					}
				}

				// create the current selection tree entry
				currentElements[childCount] = new GENTICS.Utils.RangeTree();
				currentElements[childCount].domobj = this;
				currentElements[childCount].type = type;
				if (type == 'partial') {
					currentElements[childCount].startOffset = startOffset;
					currentElements[childCount].endOffset = endOffset;
				}

				// now do the recursion step into the current object
				currentElements[childCount].children = that.recursiveGetRangeTree(this);

				// check whether a selection was found within the children
				if (currentElements[childCount].children.length > 0) {
					for (i = 0; i < currentElements[childCount].children.length; ++i) {
						switch (currentElements[childCount].children[i].type) {
						case 'none':
							noneFound = true;
							break;
						case 'full':
							fullFound = true;
							break;
						case 'partial':
							partialFound = true;
							break;
						}
					}

					if (partialFound || (fullFound && noneFound)) {
						// found at least one 'partial' DOM object in the children, or both 'full' and 'none', so this element is also 'partial' contained
						currentElements[childCount].type = 'partial';
					} else if (fullFound && !partialFound && !noneFound) {
						// only found 'full' contained children, so this element is also 'full' contained
						currentElements[childCount].type = 'full';
					}
				}

				childCount++;
			});

			// extra check for collapsed selections at the end of the current element
			if (this.isCollapsed() && currentObject === this.startContainer && this.startOffset == currentObject.childNodes.length) {
				currentElements[childCount] = new GENTICS.Utils.RangeTree();
				currentElements[childCount].type = 'collapsed';
				currentElements[childCount].domobj = undefined;
			}

			return currentElements;
		},

		/**
		 * Find certain the first occurrence of some markup within the parents of either the start or the end of this range.
		 * The markup can be identified by means of a given comparator function. The function will be passed every parent (up to the eventually given limit object, which itself is not considered) to the comparator function as this.
		 * When the comparator function returns boolean true, the markup found and finally returned from this function as dom object.<br/>
		 * Example for finding an anchor tag at the start of the range up to the active editable object:<br/>
		 * <pre>
		 * range.findMarkup(
		 *   function() {
		 *     return this.nodeName.toLowerCase() == 'a';
		 *   },
		 *   jQuery(Aloha.activeEditable.obj)
		 * );
		 * </pre>
		 * @param {function} comparator comparator function to find certain markup
		 * @param {jQuery} limit limit objects for limit the parents taken into consideration
		 * @param {boolean} atEnd true for searching at the end of the range, false for the start (default: false)
		 * @return {DOMObject} the found dom object or false if nothing found.
		 * @method
		 */
		findMarkup: function (comparator, limit, atEnd) {
			var container = atEnd ? this.endContainer : this.startContainer,
				parents,
				i,
				len;
			limit = limit ? limit[0] : null;
			if (!container) {
				return;
			}
			parents = selfAndParentsUntil(container, limit);
			for (i = 0, len = parents.length; i < len; i++) {
				if (comparator.apply(parents[i])) {
					return parents[i];
				}
			}
			return false;
		},

		/**
		 * Get the text enclosed by this range
		 * @return {String} the text of the range
		 * @method
		 */
		getText: function () {
			if (this.isCollapsed()) {
				return '';
			}
			return this.recursiveGetText(this.getRangeTree());
		},

		recursiveGetText: function (tree) {
			if (!tree) {
				return '';
			}
			var that = this,
			    text = '';
			jQuery.each(tree, function () {
				if (this.type == 'full') {
					// fully selected element/text node
					text += jQuery(this.domobj).text();
				} else if (this.type == 'partial' && this.domobj.nodeType === 3) {
					// partially selected text node
					text += jQuery(this.domobj).text().substring(this.startOffset, this.endOffset);
				} else if (this.type == 'partial' && this.domobj.nodeType === 1 && this.children) {
					// partially selected element node
					text += that.recursiveGetText(this.children);
				}
			});
			return text;
		}
	});

	/**
	 * @namespace GENTICS.Utils
	 * @class RangeTree
	 * Class definition of a RangeTree, which gives a tree view of the DOM objects included in this range
	 * Structure:
	 * <pre>
	 * +
	 * |-domobj: <reference to the DOM Object> (NOT jQuery)
	 * |-type: defines if this node is marked by user [none|partial|full|collapsed]
	 * |-children: recursive structure like this
	 * </pre>
	 */
	GENTICS.Utils.RangeTree = Class.extend({
		/**
		 * DOMObject, if the type is one of [none|partial|full], undefined if the type is [collapsed]
		 * @property domobj
		 * @type {DOMObject}
		 */
		domobj: {},

		/**
		 * type of the participation of the dom object in the range. Is one of:
		 * <pre>
		 * - none the DOMObject is outside of the range
		 * - partial the DOMObject partially in the range
		 * - full the DOMObject is completely in the range
		 * - collapsed the current RangeTree element marks the position of a collapsed range between DOM nodes
		 * </pre>
		 * @property type
		 * @type {String}
		 */
		type: null,

		/**
		 * Array of RangeTree objects which reflect the status of the child elements of the current DOMObject
		 * @property children
		 * @type {Array}
		 */
		children: []
	});

	return GENTICS.Utils.RangeObject;
});
